package time.providers.sql.query;
import haxe.macro.Expr;
import time.compiler.query.Data;
import time.compiler.query.Solver;
import time.compiler.types.Data;
import time.compiler.types.TypeBuilder;
import time.providers.sql.query.Data;
import time.providers.sql.query.Solvers;
import time.providers.sql.query.SqlGenerator;

using time.compiler.types.Data;
/**
 * 
 * @author waneck
 */

class Parser extends time.providers.base.compiler.Generator
{
	//current query level used
	private var query:SqlQuery;
	//current joins array. if an extra join is needed, here is where we add it
	private var joins:Array<{alias:String, join:SqlJoin}>;
	
	// VARIABLES //
	//all current variables declared. The array is to emulate the current stack position
	private var vars:Array< Hash<ParserVariable> >;
	//the self variable. useful if for example a function returns itself (like filter), or to redefine the self
	private var self:Variable;
	
	// ALIASES //
	//this is internally used to name all table aliases following a pattern
	//TODO disallow creation of variables using that same pattern on query.Parser
	private var aliasCount:Int;
	//this is to know at which level an alias is declared, and their accessibility on each level
	private var aliases:Hash<{ declaredLevel:Int, source:SqlJoinSource, parentAlias:Null<String> /*alias*/, levels:IntHash<Array<String>> }>; //levels is an IntHash of Array of String. For each level, it has an array of all fields transported to this level. an empty array means it has all fields
	
	//this is here so we can know in what subquery a variable(alias) was declared.
	//the further away a variable is, the more subqueries' select statement must be changed
	//so we can select the var 
	private var levels:Array<SqlQuery>;
	
	
	
	private var cache:Hash<SqlSolver>;
	
	private function new() 
	{
		super();
	}
	
	public override function parse(query:QueryExpr) : Expr
	{
		if (cache == null)
		{
			cache = new Hash();
			for (s in this.solvers)
				cache.set(s.cname, cast(s, SqlSolver));
		}
		
		query = null;
		joins = null;
		
		vars = [];
		self = null;
		
		aliasCount = 0;
		aliases = new Hash();
		
		levels = [];
	}
	
	
	public function pushBlock()
	{
		vars.push(new Hash());
	}
	
	public function popBlock()
	{
		vars.pop();
	}
	
	public function lookup(name:String)
	{
		var len = vars.length;
		for (i in 0...len)
		{
			var cur = vars[len - i];
			if (cur != null && cur.exists(name))
				return cur.get(name);
		}
		
	}
	
	private function getAlias(src:SqlJoinSource, parentAlias:Null<String>):String
	{
		var ret = "t" + (aliasCount++) + "_";
		this.aliases.set(ret, { declaredLevel:getLevel(), source:src, parentAlias:parentAlias } );
		
		return ret;
	}
	
	private function join(leftField:Variable, rightField:String, joinOp:SqlJoinOp, tbl:SqlJoinSource):String
	{
		var parentAlias = 
			if (joins.length != 0)
			{
				joins[joins.length - 1].alias;
			} else {
				query.from[0].alias;
			};
		
		var alias = getAlias(tbl, parentAlias);
		joins.push( { alias:alias, join:SClassicJoin(leftField, VColumn(alias, rightField), joinOp, tbl) } );
		
		return alias;
	}
	
	private function getLevel() : Int
	{
		return levels.length - 1;
	}
	
	
	private function newSqlQuery(entryPoint:SqlJoinSource, parentAlias):Void
	{
		var ep = {
			alias:getAlias(entryPoint, parentAlias),
			from:entryPoint,
			joins:[]
		};
		
		this.joins = ep.joins;
		
		var level = levels.length;
		var ret = {
			select:null,
			from:[ep],
			where:null,
			aggregator:null,
			having:null,
			order:null,
			limit:null,
			level:level
		};
		
		levels.push(ret);
		
		this.query = ret;
	}
	
	public function entryParse(query:QueryExpr) : Void
	{
		switch(query.expr)
		{
			case QCall(e1, s, ecall, solver):
				localParse(e1);
				var s = getSolver(solver);
				s.handleCall(e1, s, ecall, query.t, this);
			case QSelect(ep):
				if (this.query != null)
				{
					throw "Multiple entry points!" + query.pos;
				}
				
				newSqlQuery(STable(TypeBuilder.getData(ep)), null);
			default: throw "Invalid expr at this level " + query.pos;
		}
	}
	
	private function handleBuiltin(op:BuiltinOp, ecall:Array<QueryExpr>) : Void
	{
		/*
		BIs; <- clsname IN (all, clsname, subclasses)
		BCast; <- where -> clsname IN (all,clsname,subclasses)
		BEnumName; <- 
		BParent; //parent() -> for parent/child relations TODO: support this on DefaultSolver
		*/
	}
	
	private function newVar(p:ParserVariable) : SqlExpr
	{
		vars[this.vars.length - 1].set(p.variable, p);
		
		return p.expr;
	}
	
	private function unwrapToVariable(expr:SqlExpr) : Variable
	{
		switch(expr)
		{
			case SValue(v):
				switch(v)
				{
					case VVariable(v):
						return v;
				}
		}
		
		//TODO refactor this error
		throw "assert";
	}
	
	private function newThisVar(name:String, v:Variable) : SqlExpr
	{
		var expr = SValue(VVariable(v));
		
		vars[this.vars.length - 1].set(p.variable, p);
		self = v;
		
		return expr;
	}
	
	private function addColumnToThis(column:String, isStruct:Bool):Variable
	{
		if (self == null)
			throw "Null self";
		
		return switch(self)
		{
			case VStruct(_, columns):
				columns.push(column);
				
				self;
			case VColumn(table, col):
				if (!isStruct)
					throw "Table '" + table +"''s '" + col + "' was believed to be a basic type, but the field " + column + " was accessed";
				
				self = VStruct(table, [col, column]);
			case VName(v):
				self = VColumn(v, column);
		}
	}
	
	private function getKeyField(t:DBType):String
	{
		return switch(t.follow())
		{
			case DBInst(i):
				i.idField
			case DBEnum(e):
				
		}
	}
	
	function getExprAlias(e) : String
	{
		return switch(unwrapToVariable(e))
			{
				case VName(n):
					n;
				default:
					throw "a relation cannot derive from a column. It must derive from a table alias";
			};
	}
	
	/**
	 * Evaluates all descending expressions, and reduces them to only SqlExpr's
	 * All descend operations result in another join config (if the join doesn't already exist (TODO))
	 * And redirect call exprs to solvers (and get their returning value)
	 * 
	 * @param	query
	 * @return
	 */
	public function parseToExpr(query:QueryExpr) : Null<SqlExpr>
	{
		return switch(query.expr)
		{
			case QVar(name, expr):
				var v = {variable:name, expr:parseToExpr(expr)};
				newVar(v);
				
				SValue(VNull); //var is always void
				
			case QDescend(expr, field, mk):
				//in order to make a proper descend expr,
				//we need to know what kind of field it is.
				//if it's a value type, ???
				//if it's a relation, we need to know if it's to many or not
				//if it's a reference, we need to know if it's also to many or not
				// 
				//if it's to many, create a new join and return a var that refers to its alias
				//if it's not to many, also create a new join and return a var that refers to its alias
				//if it's a value type, return a var that refers to current alias + value type (VStruct)
				// Why is to many and not handled the same way?
				//Because on SQL a to-many relation is handled the same way (JOIN). What will differ is the way we handle it on the first parser level (not call directly the vars)
				
				//we'll likely discard this parsed expr because what we really need is to "self" be properly written.
				//the real path the variable is making is irrelevant to us, as on sql everything will be only 
				//a table and a column
				var e = parseToExpr(expr);
				return switch(mk)
				{
					case MValueType:
						switch(query.t.follow())
						{
							//if it's a basic type, we're in the end of the column path (we can't advance more)
							case DBBasic(_):
								SValue(VVariable(addColumnToThis(field, false)));
							default:
								SValue(VVariable(addColumnToThis(field, true)));
						}
					case MRelation(rel):
						switch(rel)
						{
							//if it's a parent->child relation, the field is always on the parent
							//TODO abstract off all common code here
							// TODO I'm only considering that it is possible only Class->Class parent/child relationship. Could there be ValueType->Class ? It seems not.
							case ParentToChild:
								var parentAlias = getExprAlias(e);
								
								var alias = getAlias(STable(query.t), parentAlias);
								var joinExpr = SBinop(QOpEq, 
									QValue(VVariable(VColumn(parentAlias, field))),
									QValue(VVariable(VColumn(alias, getKeyField(query.t))))
								);
								
								this.joins.push( { alias:alias, join:SExprJoin(joinExpr, LeftJoin, STable(query.t)) } );
								
								SValue(VVariable(VName( alias )));
							case ParentToChildren:
								var parentAlias = getExprAlias(e);
								
								var alias = getAlias(STable(query.t), parentAlias);
								var joinExpr = SBinop(QOpEq, 
									QValue(VVariable(VColumn(parentAlias, getKeyField( expr.t )))), //<< THIS. how to get the DBType from the alias?
									QValue(VVariable(VColumn(alias, "__ref_parent__")))
								);
								
								this.joins.push( { alias:alias, join:SExprJoin(joinExpr, LeftJoin, STable(query.t)) } );
								
								SValue(VVariable(VName( alias )));
							case ChildToParent(parentField):
								var childAlias = getExprAlias(e);
								
								var alias = getAlias(STable(query.t), childAlias);
								var joinExpr = SBinop(QOpEq, 
									QValue(VVariable(VColumn(childAlias, getKeyField(query.t)))),
									QValue(VVariable(VColumn(alias, field)))
								);
								
								this.joins.push( { alias:alias, join:SExprJoin(joinExpr, LeftJoin, STable(query.t)) } );
								
								SValue(VVariable(VName( alias )));
							case ChildrenToParent(parentField):
								var childAlias = getExprAlias(e);
								
								var alias = getAlias(STable(query.t), childAlias);
								var joinExpr = SBinop(QOpEq, 
									QValue(VVariable(VColumn(childAlias, "__ref_parent__"))),
									QValue(VVariable(VColumn(alias, getKeyField(query.t))))
								);
								
								this.joins.push( { alias:alias, join:SExprJoin(joinExpr, LeftJoin, STable(query.t)) } );
								
								SValue(VVariable(VName( alias )));
						}
					//TODO again, could there be ValueType->Class references? it seems to be possible, though a little complicated (the name would have to reference to ONE actual instance of the ValueType location. Also it's not clear how will ids work on this scenario)
					case MReference(to, opt):
						var dbpath = switch(expr.t.follow())
						{
							case DBInst(i):
								i.dbPath;
							default:
								throw Assert; //only classes can be referenced
						}
						
						var parentAlias = getExprAlias(e);
						
						var refName = dbpath + "___rel___" + field;
						var refAlias = join(VColumn(parentAlias, getKeyField(expr.t)), dbpath, LeftJoin, SInternalTable(refName));
						var alias = join(VColumn(refAlias, field + "_ref"), getKeyField(to), LeftJoin, STable(to));
						
						SValue(VVariable(VName( alias )));
					
					case MBackReference(to, refField, opts):
						var dbpath = switch(to.follow())
						{
							case DBInst(i):
								i.dbPath;
							default:
								throw Assert; //only classes can be referenced
						}
						
						var toAlias = getExprAlias(e);
						var refName = dbpath + "___rel___" + refField;
						var refAlias = join(VColumn(toAlias, getKeyField(expr.t)), refField + "_ref", LeftJoin, SInternalTable(refName));
						var alias = join(VColumn(refAlias, dbpath), getKeyField(to), LeftJoin, STable(to));
						
						SValue(VVariable(VName( alias )));
					
					case MSerialized:
						throw Assert;
				}
			
			case QBinop(op, e1, e2):
				SBinop(op, parseToExpr(e1), parseToExpr(e2));
			case QUnop(op, e):
				SUnop(op, parseToExpr(e));
			case QCall(e1, s, ecall, solver):
				localParse(e1);
				var s = getSolver(solver);
				s.handleCall(e1, s, ecall, query.t, this);
			case QBuiltin(op, ecall):
				switch(op)
				{
					case BCast:
						//when we cast, we are actually filtering only the
						//records that are of a certain type
						//ecall[0] -> expr
						//ecall[1] -> QConst(QCType())
						
						//get the type to cast
						var t = query.t;
						
						//add a join from ecall[0] to type's t
						var expr = parseToExpr(ecall[0].expr);
						
					case BIs:
						//the same thing, but we only return the actual boolean expr
					case BEnumName:
						//get the special enum field from ecall[0]
				}
			/*
			
			QVar(name:String, expr:QueryExpr);
			QDescend(expr:QueryExpr, field:String, mk:MemberKind); //field
			QBinop(op:QueryOp, e1:QueryExpr, e2:QueryExpr);
			QUnop(op:QueryUnop, e:QueryExpr);
			QCall(e1:QueryExpr, s:String, ecall:Array<QueryExpr>, solver:Null<String>);
			QBuiltin(op:BuiltinOp, ecall:Array<QueryExpr>);
			QConst(c:QueryConst);
			QBlock(a:Array<QueryExpr>);
			QPar(e:QueryExpr);
			QFunction(f:QueryFunction);
			QSelect(entryPoint:QueryEntryPoint);
			
			*/
		}
	}
	
	
	public function setFilter(filter:SqlExpr) : Void
	{
		//1st of all, check if there is an aggregator
			//if there is, see if HAVING is already taken
			//TODO check if any parts of the short-circuit arguments can be passed to WHERE clause (do not depend on GROUP BY statements)
				//if it is, add an AND clause and put the rest of the filter there
				//if it isn't, just put the expr in the having clause
			
			//if there isn't, check if where is taken
				//if it is, add a AND clause and put the rest of the filter there
				//if it isn't, just put the expr in the where clause
			
	}
	
	public function setAggregator() : Void
	{
		//when do we need a new query wrapper?
		//basically, when we are referencing different fields (aggregating by different fields)
		
	}
	
	public function setOrderBy() : Void
	{
		//when do we need a new query wrapper?
	}
	
	
	private function getSolver(cname:String):SqlSolver
	{
		var g = cache.get(cname);
		if (g != null)
			return g;
			
		var s = Type.createInstance(Type.resolveClass(cname), []);
		cache.set(cname, s);
		return cast(s, SqlSolver);
	}
	
	
}

typedef ParserFriend =
{
	private var query:SqlQuery;
	//private var supportsANYSubqueries:Bool;
}
